home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-games-data / gnome_sudoku / main.py < prev    next >
Encoding:
Python Source  |  2009-04-14  |  37.9 KB  |  1,002 lines

  1. # -*- coding: utf-8 -*-
  2. try:
  3.     import pygtk
  4.     pygtk.require('2.0')
  5. except ImportError, err:
  6.     print ("PyGTK not found. Please make sure it is installed properly and referenced in your PYTHONPATH environment variable.")
  7.  
  8. import gtk, gobject, gtk.glade, pango
  9. import os, os.path
  10. from gtk_goodies import gconf_wrapper, Undo, dialog_extras, image_extras
  11. import gsudoku, sudoku, saver, sudoku_maker, printing, sudoku_generator_gui
  12. import game_selector
  13. import time, threading
  14. from gettext import gettext as _
  15. from gettext import ngettext
  16. from defaults import *
  17. from timer import ActiveTimer
  18. from simple_debug import simple_debug,options
  19. from dialog_swallower import SwappableArea
  20. import LaunchpadIntegration
  21.  
  22. icon_factory = gtk.IconFactory()
  23. STOCK_PIXBUFS = {}
  24. for filename,stock_id in [('footprints.png','tracks'),]:
  25.     try:
  26.         pb = gtk.gdk.pixbuf_new_from_file(os.path.join(IMAGE_DIR,filename))
  27.     except gobject.GError, e:
  28.         print 'Failed to load pixbuf: %s' % e
  29.         continue
  30.     STOCK_PIXBUFS[stock_id]=pb
  31.     iconset = gtk.IconSet(pb)
  32.     icon_factory.add(stock_id,iconset)
  33.     icon_factory.add_default()
  34.  
  35. gtk.stock_add([('tracks',
  36.                 _('Track moves'),
  37.                 0,0,""),])
  38.  
  39. try:
  40.     STOCK_FULLSCREEN = gtk.STOCK_FULLSCREEN
  41. except:
  42.     STOCK_FULLSCREEN = _('Full Screen')
  43.  
  44. def inactivate_new_game_etc (fun):
  45.     def _ (ui, *args, **kwargs):
  46.         paths = [
  47.             '/MenuBar/Game/New',
  48.             '/MenuBar/Game/Print',
  49.             # undo/redo is handled elsewhere as it can't simply be turned on/off.
  50.             '/MenuBar/Edit/Clear',
  51.             '/MenuBar/Edit/ClearNotes',
  52.             '/MenuBar/View/ToggleToolbar',
  53.             '/MenuBar/Tools/ShowPossible',
  54.             '/MenuBar/Tools/AutofillCurrentSquare',
  55.             '/MenuBar/Tools/Autofill',
  56.             '/MenuBar/Tools/AlwaysShowPossible',
  57.             '/MenuBar/Tools/ShowImpossibleImplications',
  58.             '/MenuBar/Tools/Tracker',
  59.             '/MenuBar/Game/PuzzleInfo',
  60.             ]
  61.         for p in paths:
  62.             action = ui.uimanager.get_action(p)
  63.             if not action: action = ui.uimanager.get_widget(p)
  64.             if not action: print 'No action at path',p
  65.             else: action.set_sensitive(False)
  66.         ret = fun(ui,*args,**kwargs)
  67.         for p in paths:
  68.             action = ui.uimanager.get_action(p)
  69.             if not action: action = ui.uimanager.get_widget(p)
  70.             if not action: print 'No action at path',p
  71.             else: action.set_sensitive(True)
  72.         return ret
  73.     return _
  74.  
  75. class UI (gconf_wrapper.GConfWrapper):
  76.     ui='''<ui>
  77.     <menubar name="MenuBar">
  78.       <menu name="Game" action="Game">
  79.         <menuitem action="New"/>
  80.         <separator/>
  81.         <menuitem action="PuzzleInfo"/>
  82.         <separator/>        
  83.         <menuitem action="Print"/>
  84.         <menuitem action="PrintMany"/>
  85.         <separator/>
  86.         <menuitem action="Close"/>
  87.       </menu>
  88.       <menu action="Edit">
  89.         <menuitem action="Undo"/>
  90.         <menuitem action="Redo"/>
  91.         <separator/>
  92.         <menuitem action="Clear"/>
  93.         <menuitem action="ClearNotes"/>        
  94.       </menu>
  95.       <menu action="View">
  96.         <menuitem action="FullScreen"/>
  97.         <separator/>
  98.         <menuitem action="ToggleToolbar"/>
  99.         <menuitem action="ToggleHighlight"/>        
  100.       </menu>
  101.       <menu action="Tools">
  102.         <menuitem action="ShowPossible"/>
  103.         <menuitem action="AutofillCurrentSquare"/>
  104.         <menuitem action="Autofill"/>
  105.         <separator/>
  106.         <menuitem action="AlwaysShowPossible"/>
  107.         <menuitem action="ShowImpossibleImplications"/>
  108.         <separator/>
  109.         <menuitem action="Generator"/>
  110.         <menuitem action="BackgroundGenerator"/>
  111.         <separator/>
  112.         <menuitem action="Tracker"/>
  113.         </menu>
  114.       <menu action="Help">
  115.         <menuitem action="ShowHelp"/>
  116.         <placeholder name='LaunchpadItems'/>
  117.         <menuitem action="About"/>
  118.       </menu>
  119.     </menubar>
  120.     <toolbar name="Toolbar">
  121.       <toolitem action="New"/>
  122.       <toolitem action="Print"/>
  123.       <separator/>
  124.       <toolitem action="Undo"/>
  125.       <toolitem action="Redo"/>
  126.       <separator/>
  127.       <toolitem action="ShowPossible"/>
  128.       <toolitem action="AutofillCurrentSquare"/>      
  129.       <separator/>
  130.       <toolitem action="ToggleHighlight"/>
  131.       <toolitem action="Tracker"/>
  132.     </toolbar>
  133.     </ui>'''
  134.  
  135.     initial_prefs = {'group_size':9,
  136.                      'font_zoom':0,
  137.                      'zoom_on_resize':1,
  138.                      'always_show_hints':0,
  139.                      'player':os.environ.get('USERNAME',''),
  140.                      'difficulty':0.0,
  141.                      'minimum_number_of_new_puzzles':MIN_NEW_PUZZLES,
  142.                      'highlight':0,
  143.                      'bg_black':1,
  144.                      'bg_custom_color':'',
  145.                      'show_tracker':False,
  146.                      'width': 700,
  147.                      'height': 675,
  148.                      'auto_save_interval':60 # auto-save interval in seconds...
  149.                      }    
  150.  
  151.     @simple_debug
  152.     def __init__ (self, run_selector=True):
  153.         """run_selector means that we start our regular game.
  154.  
  155.         For testing purposes, it will be convenient to hand a
  156.         run_selector=False to this method to avoid running the dialog
  157.         and allow a tester to set up a game programmatically.
  158.         """
  159.         gconf_wrapper.GConfWrapper.__init__(self,
  160.                                             gconf_wrapper.GConf('gnome-sudoku')
  161.                                             )
  162.         self.setup_gui()
  163.         self.timer = ActiveTimer(self.w)
  164.         self.won = False
  165.         # add the accelerator group to our toplevel window
  166.         self.worker_connections=[]
  167.         # setup sudoku maker...
  168.         self.sudoku_maker = sudoku_maker.SudokuMaker()
  169.         self.sudoku_tracker = saver.SudokuTracker()
  170.         # generate puzzles while our use is working...        
  171.         self.show()
  172.         if run_selector:
  173.             self.do_stop()
  174.             if self.select_game():
  175.                 # If this return True, the user closed...
  176.                 self.quit = True
  177.             else:
  178.                 self.quit = False
  179.                 # Generate puzzles in background...
  180.                 if self.gconf['generate_puzzles_in_background']:
  181.                     gobject.timeout_add(1000,lambda *args: self.start_worker_thread() and True)
  182.         
  183.  
  184.     @inactivate_new_game_etc
  185.     def select_game (self):
  186.         self.tb.hide()
  187.         choice = game_selector.NewOrSavedGameSelector().run_swallowed_dialog(self.swallower)
  188.         if not choice:
  189.             return True
  190.         self.timer.start_timing()
  191.         if choice[0] == game_selector.NewOrSavedGameSelector.NEW_GAME:
  192.             self.gsd.change_grid(choice[1],9)
  193.         if choice[0] == game_selector.NewOrSavedGameSelector.SAVED_GAME:
  194.             saver.open_game(self,choice[1])
  195.         if self.gconf['show_toolbar']: 
  196.             self.tb.show()
  197.         if self.gconf['always_show_hints']:
  198.             self.gsd.update_all_hints()
  199.         if self.gconf['highlight']:
  200.             self.gsd.toggle_highlight(True)
  201.  
  202.  
  203.     def show (self):
  204.         self.gsd.show()
  205.         self.w.show()
  206.  
  207.     def setup_gui (self):
  208.         self.initialize_prefs()
  209.         self.setup_main_window()
  210.         self.gsd = gsudoku.SudokuGameDisplay()
  211.         self.gsd.connect('puzzle-finished',self.you_win_callback)        
  212.         self.setup_color()
  213.         self.setup_actions()
  214.         self.setup_undo()
  215.         self.setup_autosave()
  216.         self.w.add_accel_group(self.uimanager.get_accel_group())
  217.         # Add launchpad integration
  218.         LaunchpadIntegration.set_sourcepackagename("gnome-games")
  219.         LaunchpadIntegration.add_ui(self.uimanager, "/MenuBar/Help/LaunchpadItems")
  220.         self.setup_main_boxes()        
  221.         self.setup_tracker_interface()
  222.         self.setup_toggles()        
  223.  
  224.     def setup_main_window (self):
  225.         gtk.window_set_default_icon_name('gnome-sudoku')
  226.         self.w = gtk.Window()
  227.         self.w.set_default_size(self.gconf['width'], self.gconf['height'])
  228.         self.w.set_title(APPNAME_SHORT)
  229.         self.w.connect('configure-event',self.resize_cb)
  230.         self.w.connect('delete-event',self.quit_cb) 
  231.         self.uimanager = gtk.UIManager()
  232.  
  233.     def setup_actions (self):
  234.         self.main_actions = gtk.ActionGroup('MainActions')        
  235.         self.main_actions.add_actions([
  236.             ('Game',None,_('_Game')),
  237.             ('New',gtk.STOCK_NEW,None,
  238.              '<Control>n',_('New game'),self.new_cb),
  239.             ('Print',gtk.STOCK_PRINT,None,
  240.              None,_('Print current game'),self.print_game),
  241.             ('PrintMany',gtk.STOCK_PRINT,_('Print _Multiple Sudokus'),
  242.              None,_('Print more than one sudoku at a time.'),self.print_multiple_games),
  243.             ('Close',gtk.STOCK_CLOSE,None,'<Control>w',
  244.              _('Close Sudoku'),self.quit_cb),
  245.             ('Tools',None,_('_Tools')),
  246.             ('View',None,_('_View')),
  247.             ('ShowPossible',gtk.STOCK_DIALOG_INFO,_('_Hint'),
  248.              '<Control>h',
  249.              _('Show which numbers could go in the current square.'),
  250.              self.show_hint_cb),
  251.             ('AutofillCurrentSquare',gtk.STOCK_APPLY,_('_Fill'),'<Control>f',
  252.              _('Automatically fill in the current square if possible.'),
  253.              self.auto_fill_current_square_cb),
  254.             ('Autofill',gtk.STOCK_REFRESH,_('Fill _all squares'),'<Control>a',
  255.              _('Automatically fill in all squares for which there is only one valid value.'),
  256.              self.auto_fill_cb),
  257.             ('FullScreen',STOCK_FULLSCREEN,None,
  258.              'F11',None,self.full_screen_cb),
  259.             ('PuzzleInfo',gtk.STOCK_ABOUT,_('Puzzle _Statistics'),
  260.              None,_('Show statistics about current puzzle'),
  261.              self.show_info_cb),
  262.             ('Help',None,_('_Help'),
  263.              None,None,None),
  264.             ('About',gtk.STOCK_ABOUT,None,
  265.              None,None,self.show_about),
  266.             ('ShowHelp',gtk.STOCK_HELP, _('_Contents'),
  267.              'F1',None,self.show_help),
  268.             ])
  269.         self.main_actions.add_toggle_actions([
  270.             ('AlwaysShowPossible',
  271.              None,
  272.              _('_Always show hint'),
  273.              None,
  274.              _('Always show possible numbers in a square'),
  275.              self.auto_hint_cb),
  276.             ('ShowImpossibleImplications',
  277.              None,
  278.              _('Warn about _unfillable squares'),
  279.              None,
  280.              _('Warn about squares made unfillable by a move'),
  281.              self.impossible_implication_cb),
  282.             ('Tracker','tracks',_('_Track additions'),
  283.              '<Control>T',
  284.              _('Mark new additions in a separate color so you can keep track of them.'),
  285.              self.tracker_toggle_cb,False),
  286.             ('ToggleToolbar',None,_('Show _Toolbar'),None,None,self.toggle_toolbar_cb,True),
  287.             ('ToggleHighlight',gtk.STOCK_SELECT_COLOR,_('_Highlighter'),
  288.              None,_('Highlight the current row, column and box'),self.toggle_highlight_cb,False),
  289.             ('BackgroundGenerator',None,_('Generate new puzzles _while you play'),
  290.              None,
  291.              _('Generate new puzzles in the background while you play. This will automatically pause when the game goes into the background.'),
  292.              self.toggle_generator_cb, True),
  293.             ])
  294.         
  295.         self.edit_actions = gtk.ActionGroup('EditActions')
  296.         self.edit_actions.add_actions(
  297.             [('Edit',None,_('_Edit')),
  298.              ('Undo',gtk.STOCK_UNDO,_('_Undo'),'<Control>z',_('Undo last action'), self.stop_dancer),
  299.              ('Redo',gtk.STOCK_REDO,_('_Redo'),'<Shift><Control>z',_('Redo last action')),
  300.              ('Clear',gtk.STOCK_CLEAR,_('_Clear'),'<Control>b',_("Clear entries you've filled in"),self.clear_cb),
  301.              ('ClearNotes',None,_('Clear _Notes'),None,_("Clear notes and hints"),self.clear_notes_cb),             
  302.              # Trackers...
  303.              ('Tracker%s',None,_('No Tracker'),'<Control>0',None,lambda *args: self.set_tracker(-1)),
  304.              ('Generator',None,_('_Generate new puzzles'),None,_('Generate new puzzles.'),
  305.               self.generate_puzzle_gui,),
  306.              ])
  307.         self.edit_actions.add_actions(
  308.             [('Tracker%s'%n,None,'Tracker _%s'%n,'<Control>%s'%n,None,lambda *args: self.set_tracker(n-1)) for
  309.              n in range(1,9)])
  310.         self.uimanager.insert_action_group(self.main_actions,0)
  311.         self.uimanager.insert_action_group(self.edit_actions,0)
  312.         self.uimanager.add_ui_from_string(self.ui)
  313.  
  314.     def setup_undo (self):
  315.         self.cleared = [] # used for Undo memory
  316.         self.cleared_notes = [] # used for Undo memory
  317.         # Set up our UNDO stuff
  318.         undo_widg = self.edit_actions.get_action('Undo')
  319.         redo_widg = self.edit_actions.get_action('Redo')
  320.         self.history = Undo.UndoHistoryList(undo_widg,redo_widg)
  321.         for e in self.gsd.__entries__.values():
  322.             Undo.UndoableGenericWidget(e,self.history,
  323.                                        set_method='set_value_from_undo',
  324.                                        pre_change_signal='value-about-to-change'
  325.                                        )
  326.             Undo.UndoableGenericWidget(e,self.history,
  327.                                        set_method='set_notes',
  328.                                        get_method='get_note_text',
  329.                                        signal='notes-changed',
  330.                                        pre_change_signal='value-about-to-change',
  331.                                        )
  332.  
  333.     def setup_color (self):
  334.         # setup background colors
  335.         if self.gconf['bg_custom_color']:
  336.             bgcol = self.gconf['bg_custom_color']
  337.         elif self.gconf['bg_black']:
  338.             bgcol = 'black'
  339.         else:
  340.             bgcol = None
  341.         if bgcol: self.gsd.set_bg_color(bgcol)            
  342.  
  343.     def setup_autosave (self):
  344.         gobject.timeout_add(1000*(self.gconf['auto_save_interval'] or 60), # in seconds...
  345.                             self.autosave)
  346.  
  347.     def setup_main_boxes (self):
  348.         self.vb = gtk.VBox()
  349.         # Add menu bar and toolbar...
  350.         mb = self.uimanager.get_widget('/MenuBar'); mb.show()
  351.         self.vb.pack_start(mb,fill=False,expand=False)
  352.         self.tb = self.uimanager.get_widget('/Toolbar')
  353.         self.vb.pack_start(self.tb,fill=False,expand=False)
  354.         self.main_area = gtk.HBox()
  355.         self.swallower = SwappableArea(self.main_area)
  356.         self.swallower.show()
  357.         self.vb.pack_start(self.swallower,True,padding=12)
  358.         self.main_area.pack_start(self.gsd,padding=6)
  359.         self.main_actions.set_visible(True)
  360.         self.game_box = gtk.VBox()
  361.         self.main_area.show()
  362.         self.vb.show()
  363.         self.game_box.show()
  364.         self.main_area.pack_start(self.game_box,False,padding=12)
  365.         self.statusbar = gtk.Statusbar(); self.statusbar.show()
  366.         gobject.timeout_add(500,self.update_statusbar_cb)        
  367.         self.vb.pack_end(self.statusbar,fill=False,expand=False)        
  368.         self.w.add(self.vb)
  369.  
  370.     def setup_by_hand_area (self):
  371.         # Set up area for by-hand editing...
  372.         self.by_hand_label = gtk.Label()
  373.         self.by_hand_label.set_alignment(0,0)
  374.         self.by_hand_label.set_markup('<i>%s</i>'%_('Entering custom grid...'))
  375.         self.game_box.pack_start(self.by_hand_label,False,)#padding=12)        
  376.         self.by_hand_buttonbox = gtk.HButtonBox()
  377.         self.by_hand_buttonbox.set_spacing(12)
  378.         self.by_hand_save_button = gtk.Button(_('_Play game'))
  379.         self.by_hand_save_button.connect('clicked',self.save_handmade_grid)
  380.         self.by_hand_cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
  381.         self.by_hand_cancel_button.connect('clicked',self.cancel_handmade_grid)
  382.         self.by_hand_buttonbox.add(self.by_hand_cancel_button)
  383.         self.by_hand_buttonbox.add(self.by_hand_save_button)
  384.         self.game_box.pack_start(self.by_hand_buttonbox,False,padding=18)
  385.         self.by_hand_widgets = [self.by_hand_label,self.by_hand_buttonbox]        
  386.  
  387.     def setup_toggles (self):
  388.         # sync up toggles with gconf values...
  389.         map(lambda tpl: self.gconf_wrap_toggle(*tpl),
  390.             [('always_show_hints',
  391.               self.main_actions.get_action('AlwaysShowPossible')),
  392.              ('show_impossible_implications',
  393.               self.main_actions.get_action('ShowImpossibleImplications')),
  394.              ('generate_puzzles_in_background',
  395.               self.main_actions.get_action('BackgroundGenerator')),
  396.              ('show_toolbar',
  397.               self.main_actions.get_action('ToggleToolbar')),
  398.              ('highlight',
  399.               self.main_actions.get_action('ToggleHighlight')),
  400.              ('show_tracker',
  401.               self.main_actions.get_action('Tracker')),
  402.              ])        
  403.  
  404.     @simple_debug
  405.     def start_worker_thread (self, *args):
  406.         n_new_puzzles = self.sudoku_maker.n_puzzles(new=True)
  407.         if n_new_puzzles < self.gconf['minimum_number_of_new_puzzles']:
  408.             self.worker = threading.Thread(target=lambda *args: self.sudoku_maker.work(limit=5))
  409.             self.worker_connections = [
  410.                 self.timer.connect('timing-started',self.sudoku_maker.resume),
  411.                 self.timer.connect('timing-stopped',self.sudoku_maker.pause)
  412.                 ]
  413.             self.worker.start()
  414.  
  415.     @simple_debug
  416.     def stop_worker_thread (self, *args):
  417.         if hasattr(self,'worker'):
  418.             self.sudoku_maker.stop()
  419.             for c in self.worker_connections:
  420.                 self.timer.disconnect(c)
  421.  
  422.     def stop_dancer (self, *args):
  423.         if hasattr(self, 'dancer'):
  424.              self.dancer.stop_dancing()
  425.              delattr(self, 'dancer')
  426.  
  427.     @simple_debug
  428.     def you_win_callback (self,grid):
  429.         if hasattr(self, 'dancer'):
  430.             return
  431.         self.won = True
  432.         # increase difficulty for next time.
  433.         self.gconf['difficulty']=self.gconf['difficulty']+0.1
  434.         self.timer.finish_timing()
  435.         self.sudoku_tracker.finish_game(self)
  436.         sublabel = _("You completed the puzzle in %(totalTime)s (%(activeTime)s active)")%{'totalTime': self.timer.total_time_string(),
  437.         'activeTime': self.timer.active_time_string()
  438.                 }
  439.         sublabel += "\n"
  440.         sublabel += ngettext("You got %(n)s hint","You got %(n)s hints",self.gsd.hints)%{'n':self.gsd.hints}
  441.         sublabel += "\n"
  442.         if self.gsd.impossible_hints:
  443.             sublabel += ngettext("You had %(n)s impossibility pointed out.",
  444.                                  "You had %(n)s impossibilities pointed out.",
  445.                                  self.gsd.impossible_hints)%{'n':self.gsd.impossible_hints}
  446.             sublabel += "\n"
  447.         if self.gsd.auto_fills:
  448.             sublabel += ngettext("You used the auto-fill %(n)s time",
  449.                                  "You used the auto-fill %(n)s times",
  450.                                  self.gsd.auto_fills)%{'n':self.gsd.auto_fills}
  451.         from gsudoku import GridDancer
  452.         self.dancer = GridDancer(self.gsd)
  453.         self.dancer.start_dancing()            
  454.         dialog_extras.show_message(_("You win!"),label=_("You win!"),
  455.                                    sublabel=sublabel
  456.                                    )
  457.  
  458.     @simple_debug
  459.     def initialize_prefs (self):
  460.         for k,v in self.initial_prefs.items():
  461.             try:
  462.                 self.gconf[k]
  463.             except:
  464.                 self.gconf[k]=v
  465.         self.player = self.gconf['player']
  466.  
  467.     @simple_debug
  468.     @inactivate_new_game_etc
  469.     def new_cb (self,*args):
  470.         if (self.gsd.grid and self.gsd.grid.is_changed() and not self.won):
  471.             try:
  472.                 if dialog_extras.getBoolean(
  473.                     label=_("Save this game before starting new one?"),
  474.                     custom_yes=_("_Save game for later"),
  475.                     custom_no=_("_Abandon game"),
  476.                     ):
  477.                     self.save_game()
  478.                 else:
  479.                     self.sudoku_tracker.abandon_game(self)
  480.             except dialog_extras.UserCancelledError:
  481.                 # User cancelled new game
  482.                 return
  483.         self.do_stop()
  484.         self.select_game()
  485.  
  486.             
  487.     @simple_debug
  488.     def stop_game (self):
  489.        if (self.gsd.grid
  490.             and self.gsd.grid.is_changed()
  491.             and (not self.won)):
  492.             try:
  493.                 if dialog_extras.getBoolean(label=_("Save game before closing?")):
  494.                     self.save_game(self)
  495.             except dialog_extras.UserCancelledError:
  496.                 return
  497.             self.do_stop()
  498.             
  499.     def do_stop (self):
  500.         self.stop_dancer()
  501.         self.gsd.grid = None
  502.         self.tracker_ui.reset()
  503.         self.history.clear()        
  504.         self.won = False
  505.         
  506.     @simple_debug
  507.     def resize_cb (self, widget, event):
  508.         self.gconf['width'] = event.width
  509.         self.gconf['height'] = event.height
  510.  
  511.     @simple_debug
  512.     def quit_cb (self, *args):
  513.         if (self.gsd.grid
  514.             and self.gsd.grid.is_changed()
  515.             and (not self.won)):
  516.             self.save_game(self) # autosave...
  517.         if gtk.main_level() > 1:
  518.             # If we are in an embedded mainloop, that means that one
  519.             # of our "swallowed" dialogs is active, in which case we
  520.             # have to quit that mainloop before we can quit
  521.             # properly.
  522.             if self.swallower.running:
  523.                 d = self.swallower.running
  524.                 d.response(gtk.RESPONSE_DELETE_EVENT)
  525.             gtk.main_quit() # Quit the embedded mainloop
  526.             gobject.idle_add(self.quit_cb,100) # Call ourselves again
  527.                                                # to quit the main
  528.                                                # mainloop
  529.             return
  530.         self.w.hide()
  531.         # make sure we really go away before doing our saving --
  532.         # otherwise we appear sluggish.
  533.         while gtk.events_pending():
  534.             gtk.main_iteration()
  535.         if self.won:
  536.             self.gconf['current_game']=''
  537.         if not self.won:
  538.             if not self.gsd.grid:
  539.                 self.gconf['current_game']=''            
  540.         self.stop_worker_thread()
  541.         # allow KeyboardInterrupts, which calls quit_cb outside the main loop
  542.         try:
  543.             gtk.main_quit()
  544.         except RuntimeError, e:
  545.             pass
  546.  
  547.     @simple_debug
  548.     @inactivate_new_game_etc
  549.     def enter_game_by_hand (self, *args):
  550.         self.stop_game()
  551.         self.gsd.change_grid(sudoku.InteractiveSudoku(),9)
  552.         for w in self.by_hand_widgets: w.show_all()
  553.         
  554.     @simple_debug
  555.     def save_handmade_grid (self, *args):
  556.         for w in self.by_hand_widgets: w.hide()
  557.         # this should make our active grid into our virgin grid
  558.         self.won = False
  559.         self.gsd.change_grid(self.gsd.grid,9)
  560.         self.sudoku_maker.names[self.gsd.grid.to_string()]=self.sudoku_maker.get_puzzle_name('Custom Puzzle')
  561.         self.history.clear()
  562.  
  563.     @simple_debug
  564.     def cancel_handmade_grid (self, *args):
  565.         for w in self.by_hand_widgets: w.hide()
  566.  
  567.     @simple_debug
  568.     def save_game (self, *args):
  569.     self.sudoku_tracker.save_game(self)
  570.  
  571.     @simple_debug
  572.     def zoom_in_cb (self,*args):
  573.         self.gh.change_font_size(multiplier=1.1)
  574.         self.zoom = self.zoom * 1.1        
  575.  
  576.     @simple_debug
  577.     def zoom_out_cb (self,*args):
  578.         self.gh.change_font_size(multiplier=0.9)
  579.         self.zoom = self.zoom * 0.9
  580.  
  581.     def full_screen_cb (self, *args):
  582.         if not hasattr(self,'is_fullscreen'): self.is_fullscreen = False
  583.         if self.is_fullscreen:
  584.             self.w.unfullscreen()
  585.             self.is_fullscreen = False
  586.         else:
  587.             self.w.fullscreen()
  588.             self.is_fullscreen = True
  589.         
  590.     @simple_debug
  591.     def clear_cb (self,*args):        
  592.         clearer=Undo.UndoableObject(
  593.             self.do_clear, #action
  594.             self.undo_clear, #inverse
  595.             self.history #history
  596.             )
  597.         clearer.perform()
  598.  
  599.     # add a check to stop the dancer if she is dancing
  600.     def do_clear (self, *args):
  601.         self.cleared.append(self.gsd.reset_grid())
  602.         self.stop_dancer()
  603.  
  604.     # add a check for finish in the undo to clear
  605.     def undo_clear (self, *args):
  606.         for entry in self.cleared.pop():
  607.             self.gsd.add_value_to_ui(*entry) 
  608.         if self.gsd.grid.check_for_completeness():
  609.             self.gsd.emit('puzzle-finished')
  610.  
  611.     def clear_notes_cb (self, *args):
  612.         clearer = Undo.UndoableObject(
  613.             lambda *args: self.cleared_notes.append(self.gsd.clear_notes()), #action
  614.             # clear_notes returns a list of tuples indicating the cleared notes...
  615.             # (x,y,(top,bottom)) -- this is what we need for undoing
  616.             lambda *args: [self.gsd.__entries__[t[0],t[1]].set_notes(t[2]) for t in self.cleared_notes.pop()], #inverse
  617.             self.history
  618.             )
  619.         clearer.perform()
  620.  
  621.     @simple_debug
  622.     def show_hint_cb (self, *args):
  623.         self.gsd.show_hint()
  624.  
  625.     @simple_debug
  626.     def auto_hint_cb (self, action):
  627.         if action.get_active():
  628.             self.gsd.always_show_hints = True
  629.             self.gsd.update_all_hints()
  630.         else:
  631.             self.gsd.always_show_hints = False            
  632.             self.gsd.clear_hints()
  633.  
  634.     @simple_debug
  635.     def impossible_implication_cb (self, action):
  636.         if action.get_active():
  637.             self.gsd.show_impossible_implications = True
  638.         else:
  639.             self.gsd.show_impossible_implications = False
  640.  
  641.     @simple_debug
  642.     def auto_fill_cb (self, *args):
  643.         if not hasattr(self,'autofilled'): self.autofilled=[]
  644.         if not hasattr(self,'autofiller'):
  645.             self.autofiller = Undo.UndoableObject(
  646.                 self.do_auto_fill,
  647.                 self.undo_auto_fill,
  648.                 self.history
  649.                 )
  650.         self.autofiller.perform()
  651.  
  652.     def do_auto_fill (self, *args):
  653.         self.autofilled.append(self.gsd.auto_fill())
  654.         if self.gconf['always_show_hints']:
  655.             self.gsd.update_all_hints()
  656.  
  657.     def undo_auto_fill (self, *args):
  658.         for entry in self.autofilled.pop():
  659.             self.gsd.remove(entry[0],entry[1],do_removal=True)
  660.         if self.gconf['always_show_hints']:
  661.             self.gsd.update_all_hints()
  662.  
  663.     @simple_debug
  664.     def auto_fill_current_square_cb (self, *args):
  665.         self.gsd.auto_fill_current_entry()
  666.  
  667.     @simple_debug
  668.     def setup_tracker_interface (self):
  669.         self.trackers = {}        
  670.         self.tracker_ui = TrackerBox(self)
  671.         self.tracker_ui.show_all()
  672.         self.tracker_ui.hide()
  673.         self.game_box.add(self.tracker_ui)
  674.  
  675.     @simple_debug
  676.     def set_tracker (self, n):
  677.         if self.gsd.trackers.has_key(n):
  678.             self.tracker_ui.select_tracker(n)
  679.             e = self.gsd.get_focused_entry()
  680.             if e:
  681.                 if n==-1:
  682.                     for tid in self.gsd.trackers_for_point(e.x,e.y):
  683.                         self.gsd.remove_tracker(e.x,e.y,tid)
  684.                 else:
  685.                     self.gsd.add_tracker(e.x,e.y,n)
  686.         else:
  687.             print 'No tracker ',n,'yet'
  688.         
  689.     @simple_debug
  690.     def tracker_toggle_cb (self, widg):
  691.         if widg.get_active():
  692.             self.tracker_ui.show_all()
  693.         else:
  694.             self.tracker_ui.hide()
  695.  
  696.     @simple_debug
  697.     def toggle_toolbar_cb (self, widg):
  698.         if widg.get_active(): self.tb.show()
  699.         else: self.tb.hide()
  700.  
  701.     def set_statusbar_value (self, status):
  702.         if not hasattr(self,'sbid'):
  703.             self.sbid = self.statusbar.get_context_id('game_info')
  704.         self.statusbar.pop(self.sbid)
  705.         self.statusbar.push(self.sbid, status)
  706.  
  707.  
  708.     def update_statusbar_cb (self, *args):
  709.         if not self.gsd.grid: 
  710.             self.set_statusbar_value(" ")
  711.             return True
  712.  
  713.         puzz = self.gsd.grid.virgin.to_string()
  714.  
  715.         if (puzz == "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "\
  716.                     "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "\
  717.                     "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "\
  718.                     "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "\
  719.                     "0 0 0 0 0 0 0 0 0"):
  720.             self.set_statusbar_value(" ")
  721.             return True
  722.         
  723.         if (not hasattr(self,'current_puzzle_string') or
  724.             self.current_puzzle_string != puzz):
  725.             self.current_puzzle_diff = self.sudoku_maker.get_difficulty(puzz)
  726.         self.current_puzzle_string = puzz
  727.     tot_string = _("Playing %(difficulty)s puzzle.")%{'difficulty':self.current_puzzle_diff.value_string()}
  728.         tot_string += " " + "(%1.2f)"%self.current_puzzle_diff.value
  729.         self.set_statusbar_value(tot_string)
  730.         return True
  731.  
  732.     def toggle_highlight_cb (self, widg):
  733.         if widg.get_active():
  734.             self.gsd.toggle_highlight(True)
  735.         else:
  736.             self.gsd.toggle_highlight(False)
  737.  
  738.     @simple_debug
  739.     def show_info_cb (self, *args):
  740.         if not self.gsd.grid:
  741.             dialog_extras.show_message(parent=self.w,
  742.                                        title=_("Puzzle Information"),
  743.                                        label=_("There is no current puzzle.")
  744.                                        )
  745.             return
  746.         puzzle = self.gsd.grid.virgin.to_string()
  747.         diff = self.sudoku_maker.get_difficulty(puzzle)
  748.         information = _("Calculated difficulty: ")
  749.         information += diff.value_string()
  750.         information += " (%1.2f)"%diff.value
  751.         information += "\n"
  752.         information += _("Number of moves instantly fillable by elimination: ")
  753.         information += str(int(diff.instant_elimination_fillable))
  754.         information += "\n"
  755.         information += _("Number of moves instantly fillable by filling: ")
  756.         information += str(int(diff.instant_fill_fillable))
  757.         information += "\n"
  758.         information += _("Amount of trial-and-error required to solve: ")
  759.         information += str(len(diff.guesses))
  760.         dialog_extras.show_message(parent=self.w,
  761.                                    title=_("Puzzle Statistics"),
  762.                                    label=_("Puzzle Statistics"),
  763.                                    sublabel=information)
  764.  
  765.     @simple_debug
  766.     def toggle_generator_cb (self, toggle):
  767.         if toggle.get_active():
  768.             self.start_worker_thread()
  769.         else:
  770.             self.stop_worker_thread()
  771.  
  772.     @simple_debug
  773.     def show_high_scores_cb (self, *args):
  774.         hs=game_selector.HighScores(self.sudoku_tracker)
  775.         replay_game = hs.run_dialog()
  776.         if replay_game:
  777.             self.stop_game()
  778.             self.gsd.change_grid(replay_game,9)
  779.  
  780.     @simple_debug
  781.     def autosave (self):
  782.         # this is called on a regular loop and will autosave if we
  783.         # have reason to...
  784.         if self.gsd.grid and self.gsd.grid.is_changed() and not self.won:
  785.             self.sudoku_tracker.save_game(self)
  786.     
  787.     def offer_to_load_autosaved_file (self):
  788.         pass    
  789.  
  790.     @simple_debug
  791.     def load_autosave (self, filename):
  792.         saver.unpickle_game(self,filename)
  793.         self.history.clear()
  794.         
  795.     @simple_debug
  796.     def show_about (self, *args):
  797.         about = gtk.AboutDialog()
  798.         about.set_transient_for(self.w)
  799.         about.set_name(APPNAME)
  800.         about.set_version(VERSION)
  801.         about.set_copyright(COPYRIGHT)
  802.     about.set_license(LICENSE[0] + '\n\n' + LICENSE[1] + '\n\n' +LICENSE[2])
  803.     about.set_wrap_license(True)
  804.         about.set_comments(DESCRIPTION)
  805.         about.set_authors(AUTHORS)
  806.         about.set_website(WEBSITE)
  807.         about.set_website_label(WEBSITE_LABEL)
  808.         about.set_logo_icon_name("gnome-sudoku")
  809.         about.set_translator_credits(_("translator-credits"))
  810.         about.connect("response", lambda d, r: d.destroy())
  811.         about.show()
  812.  
  813.     @simple_debug
  814.     def show_help (self, *args):
  815.         try:
  816.             gtk.show_uri(self.w.get_screen(), "ghelp:gnome-sudoku", gtk.get_current_event_time())
  817.         except gobject.GError, e:
  818.             # FIXME: This should create a pop-up dialog
  819.             print _('Unable to display help: %s') % str(e)
  820.  
  821.     @simple_debug
  822.     def print_game (self, *args):
  823.         printing.print_sudokus([self.gsd], self.w)
  824.  
  825.     @simple_debug
  826.     def print_multiple_games (self, *args):
  827.         gp=game_selector.GamePrinter(self.sudoku_maker, self.gconf)
  828.         gp.run_dialog()
  829.  
  830.     @simple_debug
  831.     def generate_puzzle_gui (self, *args):
  832.         sudoku_generator_gui.GameGenerator(self,self.gconf)
  833.  
  834. class TrackerBox (gtk.VBox):
  835.  
  836.     @simple_debug
  837.     def __init__ (self, main_ui):
  838.         
  839.         gtk.VBox.__init__(self)
  840.         self.glade = gtk.glade.XML(os.path.join(GLADE_DIR,'tracker.glade'))
  841.         self.main_ui = main_ui
  842.         self.vb = self.glade.get_widget('vbox1')
  843.         self.vb.unparent()
  844.         self.pack_start(self.vb,expand=True,fill=True)
  845.         self.setup_actions()
  846.         self.setup_tree()
  847.         self.show_all()
  848.  
  849.     @simple_debug
  850.     def reset (self):
  851.         
  852.         for tree in self.tracker_model:
  853.             if tree[0]>-1:
  854.                 self.tracker_model.remove(tree.iter)
  855.  
  856.     @simple_debug
  857.     def setup_tree (self):
  858.         self.tracker_tree = self.glade.get_widget('treeview1')
  859.         self.tracker_model = gtk.ListStore(int,gtk.gdk.Pixbuf,str)
  860.         self.tracker_tree.set_model(self.tracker_model)
  861.         col1 = gtk.TreeViewColumn("",gtk.CellRendererPixbuf(),pixbuf=1)
  862.         col2 = gtk.TreeViewColumn("",gtk.CellRendererText(),text=2)
  863.         self.tracker_tree.append_column(col2)
  864.         self.tracker_tree.append_column(col1)
  865.         # Our initial row...
  866.         self.tracker_model.append([-1,None,_('No Tracker')])
  867.         self.tracker_tree.get_selection().connect('changed',self.selection_changed_cb)
  868.  
  869.     @simple_debug
  870.     def setup_actions (self):
  871.         self.tracker_actions = gtk.ActionGroup('tracker_actions')        
  872.         self.tracker_actions.add_actions(
  873.             [('Clear',
  874.               gtk.STOCK_CLEAR,
  875.               _('_Clear Tracker'),
  876.               None,_('Clear all moves tracked by selected tracker.'),
  877.               self.clear_cb
  878.               ),
  879.              ('Keep',None,
  880.               _('_Clear Others'),
  881.               None,
  882.               _('Clear all moves not tracked by selected tracker.'),
  883.               self.keep_cb),
  884.              ]
  885.             )
  886.         for action,widget_name in [('Clear','ClearTrackerButton'),
  887.                                    ('Keep','KeepTrackerButton'),
  888.                                    ]:
  889.             a=self.tracker_actions.get_action(action)
  890.             a.connect_proxy(self.glade.get_widget(widget_name))
  891.         self.glade.get_widget('AddTrackerButton').connect('clicked',
  892.                                                           self.add_tracker)
  893.         # Default to insensitive (they only become sensitive once a tracker is added)
  894.         self.tracker_actions.set_sensitive(False)
  895.  
  896.     @simple_debug
  897.     def add_tracker (self,*args):
  898.         tracker_id = self.main_ui.gsd.create_tracker()
  899.         pb=image_extras.pixbuf_transform_color(
  900.             STOCK_PIXBUFS['tracks'],
  901.             self.main_ui.gsd.get_tracker_color(tracker_id),
  902.             )
  903.         # select our new tracker
  904.         self.tracker_tree.get_selection().select_iter(
  905.             self.tracker_model.append([tracker_id,
  906.                                   pb,
  907.                                   _("Tracker %s")%(tracker_id+1)]
  908.                                   )
  909.             )
  910.         
  911.     @simple_debug
  912.     def select_tracker (self, tracker_id):
  913.         for row in self.tracker_model:
  914.             if row[0]==tracker_id:
  915.                 self.tracker_tree.get_selection().select_iter(row.iter)
  916.  
  917.     @simple_debug
  918.     def selection_changed_cb (self, selection):
  919.         mod,itr = selection.get_selected()
  920.         if itr: selected_tracker_id = mod.get_value(itr,0)
  921.         else: selected_tracker_id=-1
  922.         # This should be cheap since we don't expect many trackers...
  923.         # We cycle through each row and toggle it off if it's not
  924.         # selected; on if it is selected
  925.         for row in self.tracker_model:
  926.             tid = row[0]
  927.             if tid != -1: # -1 == no tracker
  928.                 self.main_ui.gsd.toggle_tracker(tid,tid==selected_tracker_id)
  929.         self.tracker_actions.set_sensitive(selected_tracker_id != -1)
  930.  
  931.     @simple_debug
  932.     def clear_cb (self, action):
  933.         mod,itr=self.tracker_tree.get_selection().get_selected()
  934.         # This should only be called if there is an itr, but we'll
  935.         # double-check just in case.
  936.         if itr:
  937.             selected_tracker_id=mod.get_value(itr,0)
  938.             self.tracker_delete_tracks(selected_tracker_id)
  939.  
  940.     @simple_debug
  941.     def keep_cb (self, action):
  942.         mod,itr=self.tracker_tree.get_selection().get_selected()
  943.         selected_tracker_id=mod.get_value(itr,0)
  944.         self.tracker_keep_tracks(selected_tracker_id)
  945.  
  946.     @simple_debug
  947.     def tracker_delete_tracks (self, tracker_id):
  948.         clearer=Undo.UndoableObject(
  949.             lambda *args: self.main_ui.cleared.append(self.main_ui.gsd.delete_by_tracker(tracker_id)),
  950.             lambda *args: [self.main_ui.gsd.add_value_to_ui(*entry) for entry in self.main_ui.cleared.pop()],
  951.             self.main_ui.history)
  952.         clearer.perform()
  953.  
  954.     @simple_debug
  955.     def tracker_keep_tracks (self, tracker_id):
  956.         clearer=Undo.UndoableObject(
  957.             lambda *args: self.main_ui.cleared.append(self.main_ui.gsd.delete_except_for_tracker(tracker_id)),
  958.             lambda *args: [self.main_ui.gsd.add_value_to_ui(*entry) for entry in self.main_ui.cleared.pop()],
  959.             self.main_ui.history)
  960.         clearer.perform()
  961.  
  962.  
  963. def start_game ():
  964.     if options.debug: print 'Starting GNOME Sudoku in debug mode'
  965.  
  966.     ##  You must call g_thread_init() before executing any other GLib
  967.     ##  functions in a threaded GLib program.
  968.     gobject.threads_init()
  969.  
  970.     if options.profile:
  971.         options.profile = False
  972.         profile_me()
  973.         return
  974.  
  975.     u = UI()
  976.     if not u.quit:
  977.         try:
  978.             gtk.main()        
  979.         except KeyboardInterrupt:
  980.             # properly quit on a keyboard interrupt...
  981.             u.quit_cb()
  982.  
  983. def profile_me ():
  984.     print 'Profiling GNOME Sudoku'
  985.     import tempfile,os.path
  986.     import hotshot, hotshot.stats
  987.     pname = os.path.join(tempfile.gettempdir(),'GNOME_SUDOKU_HOTSHOT_PROFILE')
  988.     prof = hotshot.Profile(pname)
  989.     prof.runcall(start_game)
  990.     stats = hotshot.stats.load(pname)
  991.     stats.strip_dirs()
  992.     stats.sort_stats('time','calls').print_stats()    
  993.  
  994.     
  995.         
  996.         
  997. if __name__ == '__main__':
  998.     import defaults
  999.     defaults.DATA_DIR == '/tmp/'; DATA_DIR=='/tmp/'
  1000.     
  1001.     
  1002.